<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>mvp-lite Demo — Tiles + Heightmap</title>
  <style>
    html,body,#glCanvas { width:100%; height:100%; margin:0; padding:0; display:block; background:#000; }
    #ui { position: absolute; left:10px; top:10px; z-index:10; color:#fff; font-family: monospace; }
  </style>
</head>
<body>
  <div id="ui">W/A/S/D 移动，鼠标拖拽旋转，滚轮缩放</div>
  <canvas id="glCanvas"></canvas>

<script>
/* -----------------------
   简化版 mvp-lite Demo
   功能：
   - WebGL 渲染瓦片化地形（heightmap） + 影像瓦片
   - 四叉树 LOD（基于相机距离）
   - Worker 异步生成/解码 heightmap 数据（模拟网络/解码）
   -----------------------*/

/* ---------- 配置 ---------- */
const TILE_SIZE = 32;        // 每个瓦片的格点数（size x size）
const ROOT_Z = 2;            // 根层级（2 或 3 -> 覆盖全球）
const MAX_LEVEL = 12;        // 最大细分层级
const MAX_ACTIVE_TILES = 200; // 缓存上限

const canvas = document.getElementById('glCanvas');
canvas.width = innerWidth; canvas.height = innerHeight;
const gl = canvas.getContext('webgl', {antialias:true});
if(!gl) { alert('WebGL 未支持'); throw new Error('WebGL not supported'); }

/* ---------- 简单着色器 ---------- */
const vsSource = `
attribute vec3 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
uniform mat4 u_mvp;
void main(){
  v_uv = a_uv;
  gl_Position = u_mvp * vec4(a_pos, 1.0);
}`;
const fsSource = `
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_image;
uniform vec3 u_light;
void main(){
  vec3 color = texture2D(u_image, v_uv).rgb;
  float shade = 0.6 + 0.4 * dot(normalize(vec3(0.5,0.7,0.2)), normalize(u_light));
  gl_FragColor = vec4(color * shade, 1.0);
}`;

function compileShader(src, type){
  const s = gl.createShader(type);
  gl.shaderSource(s, src);
  gl.compileShader(s);
  if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)){
    throw new Error(gl.getShaderInfoLog(s));
  }
  return s;
}
const prog = gl.createProgram();
gl.attachShader(prog, compileShader(vsSource, gl.VERTEX_SHADER));
gl.attachShader(prog, compileShader(fsSource, gl.FRAGMENT_SHADER));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);

const attribPos = gl.getAttribLocation(prog,'a_pos');
const attribUV = gl.getAttribLocation(prog,'a_uv');
const uniMVP = gl.getUniformLocation(prog,'u_mvp');
const uniImage = gl.getUniformLocation(prog,'u_image');
const uniLight = gl.getUniformLocation(prog,'u_light');

gl.uniform1i(uniImage, 0);
gl.uniform3fv(uniLight, new Float32Array([0.5,0.6,0.2]));

/* ---------- 相机（Orbit） ---------- */
let cam = {
  target: [0,0,0],
  theta: Math.PI*0.25, phi: Math.PI*0.4,
  radius: 4.0
};
function getCameraMatrix(){
  const x = cam.target[0] + cam.radius * Math.sin(cam.phi) * Math.cos(cam.theta);
  const y = cam.target[1] + cam.radius * Math.cos(cam.phi);
  const z = cam.target[2] + cam.radius * Math.sin(cam.phi) * Math.sin(cam.theta);
  return {eye:[x,y,z], center:cam.target, up:[0,1,0]};
}
function perspective(fovy, aspect, near, far){
  const f = 1.0/Math.tan(fovy/2), nf = 1/(near - far);
  const out = new Float32Array(16);
  out[0]=f/aspect; out[5]=f; out[10]=(far+near)*nf; out[11]=-1; out[14]=2*far*near*nf; out[15]=0;
  return out;
}
function lookAt(eye, center, up){
  const z0=eye[0]-center[0], z1=eye[1]-center[1], z2=eye[2]-center[2];
  let len = Math.hypot(z0,z1,z2); const zx=z0/len, zy=z1/len, zz=z2/len;
  let x0 = up[1]*zz - up[2]*zy, x1 = up[2]*zx - up[0]*zz, x2 = up[0]*zy - up[1]*zx;
  len = Math.hypot(x0,x1,x2); const xx=x0/len, xy=x1/len, xz=x2/len;
  const y0 = zy*xz - zz*xy, y1 = zz*xx - zx*xz, y2 = zx*xy - zy*xx;
  const out = new Float32Array(16);
  out[0]=xx; out[1]=y0; out[2]=zx; out[3]=0;
  out[4]=xy; out[5]=y1; out[6]=zy; out[7]=0;
  out[8]=xz; out[9]=y2; out[10]=zz; out[11]=0;
  out[12]=-(xx*eye[0]+xy*eye[1]+xz*eye[2]);
  out[13]=-(y0*eye[0]+y1*eye[1]+y2*eye[2]);
  out[14]=-(zx*eye[0]+zy*eye[1]+zz*eye[2]);
  out[15]=1;
  return out;
}
function mulMat(a,b){
  const r = new Float32Array(16);
  for(let i=0;i<4;i++) for(let j=0;j<4;j++){
    let s=0; for(let k=0;k<4;k++) s+=a[k*4+j]*b[i*4+k];
    r[i*4+j]=s;
  }
  return r;
}

/* ---------- 简单四叉树 TileManager（Web Mercator 简化） ---------- */
/* Tile ID using z,x,y. For demo we'll only render tiles around center lat=0 lon=0 */
class Tile {
  constructor(z, x, y){
    this.z=z; this.x=x; this.y=y;
    this.state='idle'; // idle, loading, ready
    this.mesh = null; // {vbo, ibo, count}
    this.texture = null;
    this.center = this.computeCenter();
    this.screenError = 999;
    this.lastUsed = Date.now();
  }
  computeCenter(){
    // convert tile x,y,z center to lon/lat using Web Mercator
    const n = Math.pow(2,this.z);
    const lon = (this.x + 0.5)/n*360 - 180;
    const lat = Math.atan(Math.sinh(Math.PI*(1 - 2*(this.y + 0.5)/n))) * 180/Math.PI;
    return {lon, lat};
  }
  key(){ return `${this.z}/${this.x}/${this.y}`; }
}

class TileManager {
  constructor(gl){
    this.gl = gl;
    this.tiles = new Map(); // key->Tile
    this.visible = [];
    this.worker = new Worker(URL.createObjectURL(new Blob([decoderWorkerCode], {type:'application/javascript'})));
    this.worker.onmessage = (e)=> this._onWorkerMessage(e.data);
  }

  requestTile(z,x,y){
    const key = `${z}/${x}/${y}`;
    let t = this.tiles.get(key);
    if(t) { t.lastUsed = Date.now(); return t; }
    t = new Tile(z,x,y);
    this.tiles.set(key, t);
    t.state = 'loading';
    // 异步请求影像纹理（HTML Image）和高度数据（通过 worker 返回）
    this._loadImagery(t);
    this._loadHeight(t);
    return t;
  }

  _loadImagery(tile){
    const url = `https://tile.openstreetmap.org/${tile.z}/${tile.x}/${tile.y}.png`;
    const img = new Image();
    img.crossOrigin = '';
    img.onload = () => {
      // 上传为 GL 纹理
      const tex = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
      gl.generateMipmap(gl.TEXTURE_2D);
      tile.texture = tex;
      if(tile.mesh) { tile.state = 'ready'; }
    };
    img.onerror = ()=> { console.warn('img load err',url); };
    img.src = url;
  }

  _loadHeight(tile){
    // 用 worker 异步生成高度数据（模拟从服务端获取高度map或 quantized-mesh 解码）
    this.worker.postMessage({cmd:'generateHeight', z:tile.z, x:tile.x, y:tile.y, size:TILE_SIZE});
  }

  _onWorkerMessage(msg){
    if(msg.cmd==='heightReady'){
      const key = `${msg.z}/${msg.x}/${msg.y}`;
      const tile = this.tiles.get(key);
      if(!tile) return;
      // build mesh from height array
      const mesh = buildHeightMesh(msg.heights, TILE_SIZE, tile);
      tile.mesh = mesh;
      if(tile.texture) tile.state='ready';
      tile.lastUsed = Date.now();
    }
  }

  update(camera){
    // determine visible tiles near camera target (simplified: find a set around lon=0)
    // Convert camera center to lon/lat (we use cam.target world 0,0 as lon/lat 0)
    const focusZ = Math.max(0, Math.floor(cam.radius<1? MAX_LEVEL : 4 + Math.floor((5 - Math.log2(cam.radius+1))*2)));
    const z = Math.min(MAX_LEVEL, focusZ + ROOT_Z);
    this.visible.length = 0;
    // for demo render a small window of tiles around 0,0
    const n = Math.pow(2, z);
    const centerX = Math.floor(n/2), centerY = Math.floor(n/2);
    const span = 2 + Math.floor(4/cam.radius);
    for(let dx=-span; dx<=span; dx++){
      for(let dy=-span; dy<=span; dy++){
        const x = (centerX + dx + n) % n;
        const y = (centerY + dy + n) % n;
        const t = this.requestTile(z,x,y);
        this.visible.push(t);
      }
    }
    // pruning LRU
    if(this.tiles.size > MAX_ACTIVE_TILES){
      const arr = Array.from(this.tiles.values()).sort((a,b)=> a.lastUsed - b.lastUsed);
      const removeCount = Math.min(50, this.tiles.size - MAX_ACTIVE_TILES);
      for(let i=0;i<removeCount;i++){
        const t = arr[i];
        this._disposeTile(t);
        this.tiles.delete(t.key());
      }
    }
  }

  _disposeTile(tile){
    if(tile.mesh){ gl.deleteBuffer(tile.mesh.vbo); gl.deleteBuffer(tile.mesh.ibo); tile.mesh=null; }
    if(tile.texture){ gl.deleteTexture(tile.texture); tile.texture=null; }
  }
}

/* ---------- Height mesh 构建 ---------- */
function buildHeightMesh(heights, size, tile){
  // heights: Float32Array length = size*size
  const grid = size;
  const positions = new Float32Array(grid*grid*3);
  const uvs = new Float32Array(grid*grid*2);
  const indices = new Uint16Array((grid-1)*(grid-1)*6);
  let pi=0, ui=0;
  for(let i=0;i<grid;i++){
    for(let j=0;j<grid;j++){
      const h = heights[i*grid+j];
      // map tile local xy -> world pos; for demo use tile center as lon/lat and map to small plane
      const s = (j/(grid-1) - 0.5) * 1.0; // -0.5..0.5
      const t = (i/(grid-1) - 0.5) * 1.0;
      positions[pi++] = (tile.center.lon / 180) * 1.0 + s * (1/Math.pow(2,tile.z));
      positions[pi++] = h * 0.15; // height scaled
      positions[pi++] = (tile.center.lat / 90) * 1.0 + t * (1/Math.pow(2,tile.z));
      uvs[ui++] = j/(grid-1);
      uvs[ui++] = i/(grid-1);
    }
  }
  let ii=0;
  for(let i=0;i<grid-1;i++){
    for(let j=0;j<grid-1;j++){
      const a = i*grid + j;
      const b = a + grid;
      indices[ii++]=a; indices[ii++]=b; indices[ii++]=a+1;
      indices[ii++]=b; indices[ii++]=b+1; indices[ii++]=a+1;
    }
  }
  // create GL buffers
  const vbo = gl.createBuffer();
  const vdata = new Float32Array((positions.length + uvs.length));
  // interleave pos+uv for simplicity
  let off=0, pOff=0, uvOff=0;
  for(let i=0;i<grid*grid;i++){
    vdata[off++]=positions[pOff++]; vdata[off++]=positions[pOff++]; vdata[off++]=positions[pOff++];
    vdata[off++]=uvs[uvOff++]; vdata[off++]=uvs[uvOff++];
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
  gl.bufferData(gl.ARRAY_BUFFER, vdata, gl.STATIC_DRAW);
  const ibo = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
  return {vbo, ibo, count: indices.length, stride:5*4};
}

/* ---------- Worker 代码（以字符串插入，demo 简化） ---------- */
const decoderWorkerCode = `
self.onmessage = function(e){
  const msg = e.data;
  if(msg.cmd==='generateHeight'){
    const size = msg.size || 32;
    // 生成简单的噪声高度（Perlin 可替换）
    const heights = new Float32Array(size*size);
    for(let i=0;i<size;i++){
      for(let j=0;j<size;j++){
        const nx = j/(size-1)-0.5, ny = i/(size-1)-0.5;
        const d = Math.sqrt(nx*nx + ny*ny);
        const h = 0.5 * Math.exp(-5*d*d) + 0.05 * Math.sin((j+i)/3.0);
        heights[i*size + j] = h + (Math.random()-0.5)*0.02;
      }
    }
    // 模拟延迟
    setTimeout(()=>{
      self.postMessage({cmd:'heightReady', z:msg.z, x:msg.x, y:msg.y, heights}, [heights.buffer]);
    }, 50 + Math.random()*120);
  }
};
`;

/* ---------- 渲染循环 ---------- */
const tileManager = new TileManager(gl);
function resize(){ canvas.width = innerWidth; canvas.height = innerHeight; gl.viewport(0,0,canvas.width,canvas.height); }
window.addEventListener('resize', resize);
resize();

// 鼠标控制
let dragging=false, lastX=0, lastY=0;
canvas.addEventListener('mousedown',(e)=>{ dragging=true; lastX=e.clientX; lastY=e.clientY; });
canvas.addEventListener('mouseup',()=>dragging=false);
canvas.addEventListener('mousemove',(e)=>{ if(!dragging) return; const dx=(e.clientX-lastX)*0.01, dy=(e.clientY-lastY)*0.01; cam.theta -= dx; cam.phi = Math.max(0.2, Math.min(Math.PI-0.2, cam.phi - dy)); lastX=e.clientX; lastY=e.clientY; });
canvas.addEventListener('wheel',(e)=>{ cam.radius *= (1 + e.deltaY*0.001); cam.radius = Math.max(0.5, Math.min(20, cam.radius)); });

const keys = {};
window.addEventListener('keydown', e=> keys[e.key.toLowerCase()] = true);
window.addEventListener('keyup', e=> keys[e.key.toLowerCase()] = false);

function updateCameraFromKeys(dt){
  const speed = 2.0 * dt;
  if(keys['w']) cam.radius -= speed;
  if(keys['s']) cam.radius += speed;
  if(keys['a']) cam.theta -= speed*0.5;
  if(keys['d']) cam.theta += speed*0.5;
}

let lastTime = performance.now();
function frame(now){
  const dt = (now - lastTime)/1000; lastTime = now;
  updateCameraFromKeys(dt);
  tileManager.update(cam);

  gl.clearColor(0.53,0.8,0.95,1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);

  // setup camera
  const aspect = canvas.width / canvas.height;
  const proj = perspective(Math.PI/3, aspect, 0.01, 100.0);
  const camm = getCameraMatrix();
  const view = lookAt(camm.eye, camm.center, camm.up);
  const mvp = mulMat(proj, view);
  gl.uniformMatrix4fv(uniMVP, false, mvp);

  // draw visible tiles
  for(const t of tileManager.visible){
    if(!t.mesh || !t.texture || t.state!=='ready') continue;
    gl.bindBuffer(gl.ARRAY_BUFFER, t.mesh.vbo);
    gl.enableVertexAttribArray(attribPos);
    gl.enableVertexAttribArray(attribUV);
    // pos (3) + uv (2) interleaved
    gl.vertexAttribPointer(attribPos, 3, gl.FLOAT, false, t.mesh.stride, 0);
    gl.vertexAttribPointer(attribUV, 2, gl.FLOAT, false, t.mesh.stride, 3*4);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, t.mesh.ibo);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, t.texture);
    gl.drawElements(gl.TRIANGLES, t.mesh.count, gl.UNSIGNED_SHORT, 0);
  }

  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>